package xyz.app.queries;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import jakarta.persistence.criteria.*;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import xyz.app.accessors.DbAccessor;
import xyz.app.accessors.DbDeleter;
import xyz.app.accessors.DbUpdater;
import xyz.app.dependencies.HasDomainMembers;
import xyz.app.AbstractDomain;

import java.util.Map;
import java.util.Optional;

@Slf4j
@Getter @Accessors(fluent = true)
public abstract class CriteriaWrapperExecute<T extends AbstractDomain> implements
        EntityGraphCreator<T>,
        WhereEqualsOrNull<T> {

    private final DbAccessor<T> accessor;
    private final CriteriaBuilder builder;
    private Root<T> root;

    @Setter
    private int maxAmendmentsExpected = 1;

    private CriteriaWrapperExecute(@NonNull DbAccessor<T> accessor) {
        this.accessor = accessor;
        builder = accessor.getEmf().getCriteriaBuilder();
    }

    public @NonNull Class<T> domainClass() {
        return accessor.domainClass();
    }

    @Override
    public Optional<HasDomainMembers<?>> hasMembers() {
        return accessor instanceof HasDomainMembers<?> hm ? Optional.of(hm) : Optional.empty();
    }

    abstract @NonNull Query createQuery(@NonNull EntityManager manager);

    public boolean executeUpdate() {

        log.info("""
                    ATTEMPT. Removal from database.
                    Type of entries to be removed: [{}].
                    Number of entries to be removed: [{}]""", domainClass().getSimpleName(), maxAmendmentsExpected);

        var manager = accessor.getEmf().createEntityManager();
        try (manager) {
            manager.getTransaction().begin();

            var count = createQuery(manager).executeUpdate();
            if (count > maxAmendmentsExpected)
                throw
                          this instanceof Delete<?> ? new MoreThanExpected.Removed()
                        : this instanceof Update<?> ? new MoreThanExpected.Updated()
                        : new RuntimeException();

            manager.getTransaction().commit();
            log.info("SUCCESS. Domain removed from database. Count: [{}]", count);
            return true;
        }

        catch (Exception e) {
            log.error("FAILURE. DOMAIN not removed from database");
            e.printStackTrace();
            manager.getTransaction().rollback();
            return false;
        }
    }

    @Getter
    public static class Update<T extends AbstractDomain> extends CriteriaWrapperExecute<T> {

        private final CriteriaUpdate<T> criteria;

        public Update(@NonNull DbUpdater<T> updater) {
            super(updater);
            criteria = builder().createCriteriaUpdate(domainClass());
            super.root = criteria.from(domainClass());
        }

        @Override
        @NonNull Query createQuery(@NonNull EntityManager manager) {
            var query = manager.createQuery(criteria);
            createGraph(manager, query);
            return query;
        }

        @Override
        public <V> @NonNull Update<T> whereEqualsOrNull(@NonNull String name, V value) {
            criteria.where(getPredicateEqualsOrNull(name, value));
            return this;
        }

        @Override
        public @NonNull Update<T> whereEqualsOrNull(@NonNull Map<@NonNull String, Object> values) {
            criteria.where(getPredicateEqualsOrNull(values));
            return this;
        }

        public @NonNull Update<T> set(@NonNull Map<@NonNull String, Object> amendments) {
            amendments.forEach(this::set);
            return this;
        }

        public @NonNull Update<T> set(@NonNull String name, Object value) {
            criteria.set(name, value);
            return this;
        }
    }

    @Getter
    public static class Delete<T extends AbstractDomain> extends CriteriaWrapperExecute<T> {

        private final CriteriaDelete<T> criteria;

        public Delete(@NonNull DbDeleter<T> deleter) {
            super(deleter);
            criteria = builder().createCriteriaDelete(domainClass());
            super.root = criteria.from(domainClass());
        }

        @Override
        @NonNull Query createQuery(@NonNull EntityManager manager) {
            return manager.createQuery(criteria);
        }

        @Override
        public <V> @NonNull Delete<T> whereEqualsOrNull(@NonNull String name, V value) {
            criteria.where(getPredicateEqualsOrNull(name, value));
            return this;
        }

        @Override
        public @NonNull Delete<T> whereEqualsOrNull(@NonNull Map<String, Object> values) {
            criteria.where(getPredicateEqualsOrNull(values));
            return this;
        }
    }

    private static class MoreThanExpected extends RuntimeException {

        private MoreThanExpected(String message) {
            super(message);
        }

        private static class Removed extends MoreThanExpected {
            public Removed() {
                super("Removed more entries from database than expected");
            }
        }
        private static class Updated extends MoreThanExpected {

            public Updated() {
                super("Updated more entries in database than expected");
            }
        }
    }
}